<html>
    <head>
        <title>多租户</title>
    </head>
    <body>
        <script>
            // 多租户

                    /*
                            多租户是一种广泛使用的架构，用于创建SaaS应用程序，其中硬件和软件资源由客户（租户）共享.

                            ABP提供了创建多租户应用程序的所有基本功能。


                            Wikipedia将多租户定义为：

                                    软件多租户是指一种软件架构,其中软件的单个实例在服务器上运行，并为多个租户提供服务。

                                    租户是一组用户，他们以特定权限共享对软件实例的公共访问权限。

                                    对于多租户架构，软件应用程序旨在为每个租户提供实例的专用共享，包括其数据、配置、用户管理、租户单个功能和非功能属性。

                                    多租户与多实例架构形成对比，其中单独的软件实例代表不同的租户运行。
                    */

                    /*
                        一、术语：  主机  与  租户

                                    典型的SaaS/多租户应用程序有两个主要方面：

                                            "租户" 是SaaS应用程序的客户，它支付费用以使用该服务。

                                            "Host"  是拥有SaaS应用程序并管理系统的公司。

                                    主机和租户条款将在文档的其余部分中用于该目的。
                    */

                    /*
                        二、 配置多租户


                                    1、AbpMultiTenancyOptions：启用/禁用多租户

                                            AbpMultiTenancyOptions是为你的应用程序启用/禁用多租户的主要选项类。

                                            多租户的配置"Configure<AbpMultiTenancyOptions>" 在领域层中进行配置的

                                            eg: 启用多租户

                                                Configure<AbpMultiTenancyOptions>(options =>
                                                {
                                                    options.IsEnabled = true;
                                                });


                                            默认情况下，多租户在ABP中被禁用。但是，当你使用启动模板创建新解决方案时，它默认启用。

                                            解决方案中的MultiTenancyConsts类有一个常量来在单个位置控制它。


                                    2、数据库体系结构

                                            ABP支持以下所有方法将租户数据存储在数据库中：

                                                    ①、单一数据库：所有租户都存储在单一数据库中。

                                                    ②、每个租户的数据库：每个租户都有一个单独的专用数据库来存储与该租户相关的数据。

                                                    ③、混合：一些租户共享一个数据库，而一些租户可能拥有自己的数据库。
                                                

                                            Saas模块（PRO）允许你为任何租户设置连接字符串（作为可选），因此你可以实现任何方法。        
                    */

                    /*
                        三、使用方法

                                  多租户系统旨在无缝工作，并使你的应用程序代码对多租户系统的存在尽可能无感知。  

                                  

                                  1、多租户接口  IMultiTenant

                                            您应该为您的实体实现IMultiTenant接口，以使它们的多租户功能就绪。


                                                (1)、如何为实体开启多租户支持?

                                                                eg: 多租户Product实体

                                                                    using System;
                                                                    using Volo.Abp.Domain.Entities;
                                                                    using Volo.Abp.MultiTenancy;

                                                                    namespace MultiTenancyDemo.Products
                                                                    {
                                                                        public class Product : AggregateRoot<Guid>, IMultiTenant
                                                                        {
                                                                            public Guid? TenantId { get; set; } //  IMultiTenant接口定义的属性

                                                                            public string Name { get; set; }

                                                                            public float Price { get; set; }
                                                                        }
                                                                    }

                                                                IMultiTenant接口只是定义了一个TenantId属性。

                                                                当您实现此接口时，当您从数据库中查询时，ABP会自动为当前租户过滤实体。

                                                                因此，您不需要在执行查询时手动添加TenantId条件。

                                                                默认情况下，租户无法访问另一个租户的数据。


                                                (2)、为什么TenantId属性可以为空？

                                                            IMultiTenant.TenantId是nullable。 
                                                            
                                                            当它为空时，表示该实体归主机端所有，而不归租户所有。

                                                            当您在系统中创建租户和主机端都使用的功能时，它很有用。

                                                            例如，IdentityUser是由Identity Module定义的实体。主机和所有租户都有自己的用户。

                                                                因此，对于主机端，用户将有一个为null的TenantId，而租户用户将有他们相关的TenantId。

                                                            注意：

                                                                如果您的实体是特定于租户的并且在主机端没有任何意义，您可以强制不允许为实体的构造函数中的TenantId设置null。


                                                (3)、什么时候设置TenantId？   
                                                                        
                                                            当您创建新实体对象时，ABP会自动为您设置TenantId。

                                                            这是在baseEntity类的构造函数中完成的（所有其他基本实体和聚合根类都派生自Entity类）。

                                                            TenantId是根据ICurrentTenant.Id属性的当前值设置的（见下一节）。

                                                            如果您为特定实体对象设置了TenantId值，它将覆盖基类设置的值。

                                                            如果您想自己设置TenantId属性，我们建议在您的实体类的构造函数中进行，并且不要再次更改（更新）它。

                                                            （实际上，更改它意味着您正在将实体从租户移动到另一个租户。如果您希望这样，您需要额外关心数据库中的相关实体）



                                   2、当前租户接口 ICurrentTenant

                                               ICurrentTenant是与"多租户基础设施" 交互的主要服务。

                                               ApplicationService、DomainService、AbpController和其他一些基类已经具有预注入的CurrentTenant属性。

                                               对于其他类型的类，您可以将ICurrentTenant注入到您的服务中。


                                               (1)、租户属性

                                                            ICurrentTenant 接口定义了以下属性：

                                                                            ①、Id（Guid）：当前租户的ID。如果当前用户是主机用户或无法从请求中确定租户，则可以为null。

                                                                            ②、Name（string）：当前租户的名称。如果当前用户是主机用户或无法从请求中确定租户，则可以为null。

                                                                            ③、如果Id不是null，则返回true。

                                               (2)、修改当前租户

                                                            BP根据ICurrentTenant.Id自动过滤资源（数据库、缓存…）。

                                                            但是，在某些情况下，您可能希望代表特定租户执行操作，通常是在主机上下文中。

                                                            ICurrentTenant.Change方法将当前租户更改放在有限的作用域中，因此您可以安全地为租户执行操作。

                                                            eg:  获取特定租户的产品计数

                                                                    using System;
                                                                    using System.Threading.Tasks;
                                                                    using Volo.Abp.Domain.Repositories;
                                                                    using Volo.Abp.Domain.Services;

                                                                    namespace MultiTenancyDemo.Products
                                                                    {
                                                                        public class ProductManager : DomainService
                                                                        {
                                                                            private readonly IRepository<Product, Guid> _productRepository;

                                                                            public ProductManager(IRepository<Product, Guid> productRepository)
                                                                            {
                                                                                _productRepository = productRepository;
                                                                            }

                                                                            // 根据前端传来的租户ID，获取该租户的产品数量
                                                                            public async Task<long> GetProductCountAsync(Guid? tenantId)
                                                                            {
                                                                                using (CurrentTenant.Change(tenantId))  // 将租户更改房子放在有限的作用域中，这样可以安全地为租户执行操作
                                                                                {
                                                                                    return await _productRepository.GetCountAsync();
                                                                                }
                                                                            }
                                                                        }
                                                                    }

                                                            Change方法可以以嵌套方式使用。它将CurrentTenant.Id恢复为using语句之后的前一个值。

                                                            当您在Change范围内使用CurrentTenant.Id时，您将获得提供给Change方法的tenantId。

                                                            因此，存储库也会获得这个tenantId并可以相应地过滤数据库查询。

                                                            ★、使用CurrentTenant.Change(null)将范围更改为主机上下文。


                                    3、数据过滤： 禁用多租户过滤器

                                                如前所述，ABP使用数据过滤系统处理租户之间的数据隔离。

                                                在某些情况下，您可能希望禁用它并对所有数据执行查询，而不过滤当前租户。

                                                eg: 获取数据库中的产品计数，包括所有租户的所有产品：

                                                            using System;
                                                            using System.Threading.Tasks;
                                                            using Volo.Abp.Data;
                                                            using Volo.Abp.Domain.Repositories;
                                                            using Volo.Abp.Domain.Services;
                                                            using Volo.Abp.MultiTenancy;

                                                            namespace MultiTenancyDemo.Products
                                                            {
                                                                public class ProductManager : DomainService
                                                                {
                                                                    private readonly IRepository<Product, Guid> _productRepository;
                                                                    private readonly IDataFilter _dataFilter;

                                                                    public ProductManager(
                                                                        IRepository<Product, Guid> productRepository,
                                                                        IDataFilter dataFilter)
                                                                    {
                                                                        _productRepository = productRepository;
                                                                        _dataFilter = dataFilter;
                                                                    }

                                                                    public async Task<long> GetProductCountAsync()
                                                                    {
                                                                        using (_dataFilter.Disable<IMultiTenant>())  // 在特定作用域范围内禁用多租户过滤器
                                                                        {
                                                                            return await _productRepository.GetCountAsync();
                                                                        }
                                                                    }
                                                                }
                                                            }

                                                有关更多信息，请参阅数据过滤文档： https://abp.io/docs/latest/framework/infrastructure/data-filtering

                                                        
                                                备注：

                                                    请注意，如果您的租户有单独的数据库，则此方法将不起作用，因为没有内置方法可以在单个数据库查询中从多个数据库进行查询。

                                                    如果需要，您应该自己处理。

                    */

                    /*
                        四、基础设施

                                  1、定义当前租户  Determining the Current Tenant 

                                         多租户应用程序的第一件事是确定运行时的当前租户。

                                         ABP为此提供了一个可扩展的 "租户解析系统".

                                         然后在Multi-Tenancy 中间件中使用"租户解析系统"来确定当前HTTP请求的当前租户。


                                         (1)、租户解析器  Tenant Resolvers 

                                                  -1-- 默认租户解析器  Default Tenant Resolver

                                                        默认提供和配置以下解析器:

                                                              ①、CurrentUserTenantResolveContributor：
                                                                        
                                                                        如果当前用户已登录，则从当前用户的声明中获取租户ID。这应该始终是安全性的第一个贡献者。

                                                              ②、QueryStringTenantResolveContributor：尝试从查询字符串参数中查找当前租户ID。参数名称默认为__tenant。

                                                              ③、HeaderTenantResolveContributor：尝试从HTTP标头中查找当前租户ID。标头名称默认为__tenant。

                                                              ④、CookieTenantResolveContributor：尝试从cookie值中查找当前租户ID。默认情况下，cookie名称为__tenant。

                                                              ⑤、RouteTenantResolveContributor：尝试从路由（URL路径）中查找当前租户ID。
                                                              
                                                                                                变量名默认为__tenant。
                                                                                                
                                                                                                如果您使用此变量定义了路由，则它可以从路由中确定当前租户。

                                        

                                                  -2-- 域、子域租户解析器  Domain/Subdomain Tenant Resolvers
                                                                            
                                                         在实际应用程序中，大多数时候您希望通过子域（如mytenant1.mydomain.com）或 整个域（如mytenant.com）来确定当前租户。

                                                         如果是这样，您可以配置 "AbpTenantResolveOptions" 来添加域租户解析器。

                                                         eg:  添加子域解析器
                                                                            
                                                                Configure<AbpTenantResolveOptions>(options =>
                                                                {
                                                                    options.AddDomainTenantResolver("{0}.mydomain.com");
                                                                });

                                                            {0} 是确定当前租户唯一名称的占位符。

                                                            将此代码添加到模块的ConfigureServices方法中。

                                                            这应该在Web/API层中完成，因为URL是与Web相关的东西。


                                                            ★、关于独立验证服务器

                                                                        Openidduce是ABP中的默认身份验证服务器（自v6.0起）。
                                                                        
                                                                        当您使用OpenIdduce时，您还必须将此代码添加到PreConfigure方法中。
                                                            
                                                                                    // using Volo.Abp.OpenIddict.WildcardDomains

                                                                                    PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
                                                                                    {
                                                                                        options.EnableWildcardDomainSupport = true;
                                                                                        options.WildcardDomainsFormat.Add("https://{0}.mydomain.com");
                                                                                    }); 

                                                                        您还必须将此代码添加到Configure方法中：

                                                                                    // using Volo.Abp.MultiTenancy;

                                                                                    Configure<AbpTenantResolveOptions>(options =>
                                                                                    {
                                                                                        options.AddDomainTenantResolver("{0}.mydomain.com");
                                                                                    });

                                                                        如果使用单独的身份验证服务器，则必须在HTTPApi.Host项目上安装
                                                                        
                                                                        [Owl.TokenWildcardIssuerValidator](https://www.nuget.org/packages/Owl.TokenWildcardIssuerValidator)：、、

                                                                                   dotnet add package Owl.TokenWildcardIssuerValidator

                                                                        然后修复.AddJwtBearer块：

                                                                                    // using using Owl.TokenWildcardIssuerValidator;

                                                                                    context.Services
                                                                                        .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                                                                                        .AddJwtBearer(options =>
                                                                                        {
                                                                                            options.Authority = configuration["AuthServer:Authority"];
                                                                                            options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
                                                                                            options.Audience = "ExampleProjectName";

                                                                                            // start of added  block
                                                                                            options.TokenValidationParameters.IssuerValidator = TokenWildcardIssuerValidator.IssuerValidator;
                                                                                            options.TokenValidationParameters.ValidIssuers = new[]
                                                                                            {
                                                                                                "https://{0}.mydomain.com:44349/" //the port may different
                                                                                            };
                                                                                            // end of added  block
                                                                                        });



                                                -3-- 自定义租户解析器

                                                            您可以添加实现自定义租户解析器，并在模块的ConfigureServices方法中配置AbpTenantResolveOptions，如下所示：
                                                            
                                                                                Configure<AbpTenantResolveOptions>(options =>
                                                                                {
                                                                                    options.TenantResolvers.Add(new MyCustomTenantResolveContributor());
                                                                                });

                                                            MyCustomTenantResolveContributor必须继承自TenantResolveContributorBase（或实现ITenantResolveContributor），
                                                            
                                                            如下所示：

                                                                                using System.Threading.Tasks;
                                                                                using Volo.Abp.MultiTenancy;

                                                                                namespace MultiTenancyDemo.Web
                                                                                {
                                                                                    public class MyCustomTenantResolveContributor : TenantResolveContributorBase
                                                                                    {
                                                                                        public override string Name => "Custom";

                                                                                        public override Task ResolveAsync(ITenantResolveContext context)
                                                                                        {
                                                                                            //TODO...
                                                                                        }
                                                                                    }
                                                                                }

                                                            租户解析器应该设置context.TenantIdOrName如果它可以确定它。如果不是，请保持原样以允许下一个解析器确定它。

                                                            context.ServiceProvider如果需要从依赖注入系统解析附加服务，可以使用。





                                   2、NGINX的问题

                                               如果您使用nginx作为反向代理服务器，HTTP标头中的__tenant可能会出现问题。

                                               因为它不允许在HTTP标头中使用下划线和其他一些特殊字符，您可能需要手动配置它。请参阅以下文档：

                                                        http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headershttp://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers

                                                

                                   3、AbpAspNetCoreMultiTenancyOptions 选项类


                                                (1)、修改"租户键"的参数名称

                                                            __tenant可以使用 "AbpAspNetCoreMultiTenancyOptions" 更改参数名称。

                                                            eg：
                                                                    services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
                                                                    {
                                                                        options.TenantKey = "MyTenantKey";  // 更改"TenantKey",参数名称
                                                                    });


                                                            如果您更改了TenantKey，请确保通过Angular客户端中的withOptions方法将其传递给provideAbpCore，如下所示：

                                                                    @NgModule({
                                                                        providers: [
                                                                            provideAbpCore(
                                                                            withOptions({
                                                                                // ...
                                                                                tenantKey: "MyTenantKey",
                                                                            })
                                                                            ),
                                                                        ],
                                                                        // ...
                                                                    })
                                                                    export class AppModule {}

                                                            如果需要访问它，可以按如下方式注入：
                                                            
                                                                    import { Inject } from '@angular/core';
                                                                    import { TENANT_KEY } from '@abp/ng.core';

                                                                    class SomeComponent {
                                                                        constructor(@Inject(TENANT_KEY) private tenantKey: string) {}
                                                                    }

                                                            但是，我们不建议更改此值，因为某些客户端可能会假定__tenant作为参数名称，并且他们可能需要手动配置。

                                                (2)、MultiTenancyMiddlewareErrorPageBuilder 用于处理不活动和不存在的租户。

                                                                默认情况下它会响应一个错误页面，您可以根据需要对其进行更改，
                                                                
                                                                例如：仅输出错误日志并继续ASP NET Core的请求管道。

                                                                        Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
                                                                        {
                                                                            options.MultiTenancyMiddlewareErrorPageBuilder = async (context, exception) =>
                                                                            {
                                                                                // 处理异常

                                                                                // Return true to stop the pipeline, false to continue.
                                                                                return true;
                                                                            };
                                                                        });

                                                (3)、关于独立验证服务器

                                                            Openidduce是ABP中的默认身份验证服务器（自v6.0起）。
                                                            
                                                            当您使用OpenIdduce时，您还必须将此代码添加到PreConfigure方法中。
                                                
                                                                        // using Volo.Abp.OpenIddict.WildcardDomains

                                                                        PreConfigure<AbpOpenIddictWildcardDomainOptions>(options =>
                                                                        {
                                                                            options.EnableWildcardDomainSupport = true;
                                                                            options.WildcardDomainsFormat.Add("https://{0}.mydomain.com");
                                                                        }); 

                                                            您还必须将此代码添加到Configure方法中：

                                                                        // using Volo.Abp.MultiTenancy;

                                                                        Configure<AbpTenantResolveOptions>(options =>
                                                                        {
                                                                            options.AddDomainTenantResolver("{0}.mydomain.com");
                                                                        });

                                4、多租户中间件

                                          多租户中间件是一个ASP.NET core 请求管道中间件，它从HTTP请求中确定当前租户并设置ICurrentTenant属性。

                                          多租户中间件通常放置在身份验证中间件（app.UseAuthentication()）下：

                                                    app.UseMultiTenancy();

                                                此中间件已在启动模板中配置，因此您通常不需要手动添加它。

                    */

                    /*
                        五、   Tenant Store 租户的存储
                        
                        
                                    ITenantStore用于从数据库中获取租户配置

                                    租户名称不区分大小写。ITenantStore将使用NormalizedName参数来获取租户，您需要使用ITenantNormalizer来规范化租户名称。


                                    1、租户管理模块

                                            租户管理模块包含在启动模板中，并实现ITenantStore接口以从数据库中获取租户及其配置。

                                            它还提供了管理租户所需的功能和UI。


                                    2、配置数据存储

                                            如果您不想使用租户管理模块，DefaultTenantStore用作ITenantStore实现。

                                            它从配置系统（IConfiguration）获取租户配置。

                                            您可以配置AbpDefaultTenantStoreOptions选项或在appsettings.json文件中设置它：

                                                eg: 在appsetings. json中定义租户

                                                    "Tenants": [
                                                        {
                                                            "Id": "446a5211-3d72-4339-9adc-845151f8ada0",
                                                            "Name": "tenant1",
                                                            "NormalizedName": "TENANT1"
                                                        },
                                                        {
                                                            "Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d",
                                                            "Name": "tenant2",
                                                            "NormalizedName": "TENANT2",
                                                            "ConnectionStrings": {
                                                                "Default": "...tenant2's db connection string here..."
                                                            }
                                                        }
                                                    ]


                                            （还是建议使用租户管理模块，当你使用ABP启动模板创建新应用程序时，该模块已预先配置）
                                                  

                                                    
                    */
        </script>
    </body>
</html>